// Tierless
// Copyright © 2010 Alphora
// This file is licensed under a modified BSD-license (see tierless_license.txt).
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

using Tierless.Framework.Entities;

namespace Tierless.Framework.Data
{
	/// <summary>
	/// Represents an observable collection of entities of a single type, searchable by entity ID.
	/// </summary>
	public class EntitySet : ObservableCollection<Entity>, IEnumerable<KeyValuePair<EntityID, Entity>>
	{
		#region Fields

		private string _entityTypeName;
		private Dictionary<EntityID, Entity> _index = new Dictionary<EntityID, Entity>(EntityIDEqualityComparer.Default);

		#endregion

		#region Properties

		/// <summary>
		/// Gets or sets the type of entities contained within the entity set.
		/// </summary>
		public string EntityTypeName
		{
			get { return _entityTypeName; }
			set
			{
				if (_entityTypeName != value)
				{
					foreach (var entity in this)
						CheckEntityTypeNamesMatch(entity.EntityTypeName, value);

					_entityTypeName = value;
				}
			}
		}

		protected Dictionary<EntityID, Entity> Index
		{
			get { return _index; }
		}

		#endregion

		#region Constructors

		/// <summary>
		/// Initializes a new instance of the EntitySet class.
		/// </summary>
		public EntitySet()
		{}

		/// <summary>
		/// Initializes a new instance of the EntitySet class.
		/// </summary>
		/// <param name="entityTypeName">The type of entities that will be contained within the entity set.</param>
		public EntitySet(string entityTypeName)
		{
			if (String.IsNullOrEmpty(entityTypeName))
				throw new ArgumentException("An entity type name is required to construct an entity set.");

			_entityTypeName = entityTypeName;
		}

		#endregion

		#region Methods

		protected void EnsureEntityTypeName(string entityTypeName)
		{
			if (_entityTypeName == null)
				_entityTypeName = entityTypeName;
		}

		protected void CheckEntityTypeNamesMatch(string entityTypeName, string entitySetEntityTypeName)
		{
			if (entityTypeName != entitySetEntityTypeName)
				throw new InvalidOperationException(String.Format("The operation cannot be completed because it would result in an entity of type '{0}' being added to the entity set, but it is declared to contain entities of type '{1}'.", entityTypeName, entitySetEntityTypeName));
		}

		protected void CheckEntityTypeNamesMatch(string entityTypeName)
		{
			CheckEntityTypeNamesMatch(entityTypeName, _entityTypeName);
		}

		/// <summary>
		/// Merges the contents of the given entity set in to this entity set.
		/// </summary>
		/// <param name="merge">The entity set containing the entities to be merged into this entity set.</param>
		public void Merge(EntitySet merge)
		{
			EnsureEntityTypeName(merge.EntityTypeName);
			CheckEntityTypeNamesMatch(merge.EntityTypeName);

			if (merge.Index.Count > 0)
				foreach (var item in (IEnumerable<KeyValuePair<EntityID, Entity>>) merge)
					Set(item.Key, item.Value);
			else
				foreach (var entity in merge)
					Add(entity);
		}

		/// <summary>
		/// Replaces the contents of this entity set with the entities in the given entity set.
		/// </summary>
		/// <param name="replace">The entity set containing the entities that will replace the contents of this entity set.</param>
		public void Replace(EntitySet replace)
		{
			EnsureEntityTypeName(replace.EntityTypeName);
			CheckEntityTypeNamesMatch(replace.EntityTypeName);

			Clear();

			foreach (var item in (IEnumerable<KeyValuePair<EntityID, Entity>>) replace)
				Add(item.Key, item.Value);
		}

		/// <summary>
		/// Returns a new entity set containing the same entities as this entity set.
		/// </summary>
		/// <returns>A new entity set containing the same entities as this entity set.</returns>
		/// <remarks>
		/// Note that this is a shallow operation, the returned entity set will contain references to the entities,
		/// not clones of the entities in this entity set.
		/// </remarks>
		public EntitySet Clone()
		{
			var result = new EntitySet(_entityTypeName);
			foreach (var entity in this)
				result.Add(entity);
			return result;
		}

		/// <summary>
		/// Returns a new entity set containing copies of the entities in this entity set.
		/// </summary>
		/// <returns>A new entity set containing copies of the entities in this entity set.</returns>
		/// <remarks>
		/// Note that this is a deep cloning operation, the returned entity set will contain copies of the entities
		/// in this entities set, recursively.
		/// </remarks>
		public EntitySet CloneDeep()
		{
			var result = new EntitySet(_entityTypeName);
			foreach (var entity in this)
				result.Add(entity.Clone());
			return result;
		}

		/// <summary>
		/// Returns the first entity in the entity set, if any, null otherwise.
		/// </summary>
		/// <returns>The first entity in the entity set, if any, null otherwise.</returns>
		public Entity First()
		{
			return Count == 0 ? null : this[0];
		}

		/// <summary>
		/// Returns the first entity in the entity set, ensuring the set contains at most one entity.
		/// </summary>
		/// <returns>The first entity in the entity set, ensuring the set contains at most one entity.</returns>
		/// <exception cref="InvalidOperationException">The entity set contains more than one entity.</exception>
		/// <remarks>
		/// This method will return null if the entity set is empty, the first entity if the set contains only
		/// a single entity, or throw an <see cref="InvalidOperationException"/> if the set contains more than one
		/// entity.
		/// </remarks>
		public Entity Only()
		{
			if (Count == 0)
				return null;

			if (Count > 1)
				throw new InvalidOperationException("The entity set contains multiple entities.");

			return this[0];
		}

		#endregion

		#region ObservableCollection

		protected override void InsertItem(int index, Entity item)
		{
			if (item != null)
			{
				EnsureEntityTypeName(item.EntityTypeName);
				CheckEntityTypeNamesMatch(item.EntityTypeName);

				InternalIndexItem(item);
			}

			base.InsertItem(index, item);
		}

		protected override void RemoveItem(int index)
		{
			var oldItem = this[index];
			base.RemoveItem(index);
			if (oldItem != null)
				InternalUnindexItem(oldItem);
		}

		private void InternalIndexItem(Entity item)
		{
			if (item.EntityType.PrimaryKey != null)
				_index.Add(item.ID, item);
		}

		private void ItemColumnChanged(Entity entity, EntityColumn column, object oldValue, object newValue)
		{
			if (column.ColumnType.IsKey && !Equals(oldValue, newValue))
			{
				InternalUnindexItem(entity);
				InternalIndexItem(entity);
			}
		}

		private void InternalUnindexItem(Entity item)
		{
			if (item.EntityType.PrimaryKey != null)
				_index.Remove(item.ID);
		}

		protected override void ClearItems()
		{
			base.ClearItems();
			_index.Clear();
		}

		protected override void SetItem(int index, Entity item)
		{
			if (item != null)
			{
				EnsureEntityTypeName(item.EntityTypeName);
				CheckEntityTypeNamesMatch(item.EntityTypeName);
			}

			var oldItem = this[index];
			base.SetItem(index, item);
			if (oldItem != null)
				InternalUnindexItem(oldItem);
			if (item != null)
				InternalIndexItem(item);
		}

		#endregion

		#region Pseudo Dictionary support

		/// <summary>
		/// Returns the entity with the given ID, if it exists in this set, null otherwise.
		/// </summary>
		/// <param name="key">The ID of the entity to be retrieved.</param>
		/// <returns>The entity with the given ID, if it exists in this set, null otherwise.</returns>
		public Entity Get(EntityID key)
		{
			Entity tempValue;
			return _index.TryGetValue(key, out tempValue) ? tempValue : null;
		}

		/// <summary>
		/// Returns the first entity in the set matching the given predicate.
		/// </summary>
		/// <param name="condition">The predicate to use to perform the matching.</param>
		/// <returns>The first entity in the set matching the given predicate.</returns>
		public Entity FindFirst(Predicate<Entity> condition)
		{
			if (condition == null)
				throw new ArgumentNullException("ACondition");

			foreach (var item in this)
			{
				if (condition(item))
					return item;
			}

			return null;
		}

		/// <summary>
		/// Adds or updates the set with the given entity.
		/// </summary>
		/// <param name="key">The ID of the entity.</param>
		/// <param name="tempValue">The entity to be added or updated.</param>
		public void Set(EntityID key, Entity tempValue)
		{
			if (ContainsKey(key))
			{
				if (tempValue != null)
				{
					EnsureEntityTypeName(tempValue.EntityTypeName);
					CheckEntityTypeNamesMatch(tempValue.EntityTypeName);
				}

				Remove(Index[key]);
				Add(key, tempValue);
			}
			else
				Add(key, tempValue);
		}

		/// <summary>
		/// Adds the given entity to the entity set.
		/// </summary>
		/// <param name="key">The ID of the entity.</param>
		/// <param name="tempValue">The entity.</param>
		/// <remarks>
		/// Note that <c>tempValue</c> may be null, indicating that the set positively indicates that an entity with that
		/// key does not exist.
		/// </remarks>
		public void Add(EntityID key, Entity tempValue)
		{
			if (tempValue != null)
				Add(tempValue);
			else
				_index.Add(key, null);
		}

		/// <summary>
		/// Includes the given entity in the set, replacing any entity that already exists by entity ID.
		/// </summary>
		/// <param name="tempValue"></param>
		public void Include(Entity tempValue)
		{
			if (ContainsKey(tempValue.ID))
				Set(tempValue.ID, tempValue);
			else
				Add(tempValue.ID, tempValue);
		}

		/// <summary>
		/// Returns true if the set contains an entry for the given entity ID.
		/// </summary>
		/// <param name="key">The ID to test.</param>
		/// <returns>True if the set contains an entry for the given entity ID.</returns>
		public bool ContainsKey(EntityID key)
		{
			return _index.ContainsKey(key);
		}

		/// <summary>
		/// Gets the collection of Keys contained in this set.
		/// </summary>
		public ICollection<EntityID> Keys
		{
			get { return _index.Keys; }
		}

		/// <summary>
		/// Removes the entry for the given key from the set.
		/// </summary>
		/// <param name="key">The ID to be removed.</param>
		/// <returns>True if the key was removed from the set, false otherwise.</returns>
		public bool RemoveKey(EntityID key)
		{
			return Remove(Get(key));
		}

		/// <summary>
		/// Attempts to retrieve the entry for the given key.
		/// </summary>
		/// <param name="key">The ID of the entry to be retrieved.</param>
		/// <param name="tempValue">The entity to be retrieved.</param>
		/// <returns>True if the entry was found in the entity set, false otherwise.</returns>
		/// <remarks>
		/// Note that it is possible for this method to return true, and yet still have a null
		/// for the resulting Entity. The reason is that the EntitySet allows the non-existence of an
		/// entity to be represented by an entry in the entity set for the EntityID, with a null for the
		/// entity value.
		/// </remarks>
		public bool TryGetValue(EntityID key, out Entity tempValue)
		{
			return _index.TryGetValue(key, out tempValue);
		}

		/// <summary>
		/// Returns a dictionary enumerator for the set.
		/// </summary>
		/// <returns>A dictionary enumerator for the set.</returns>
		IEnumerator<KeyValuePair<EntityID, Entity>> IEnumerable<KeyValuePair<EntityID, Entity>>.GetEnumerator()
		{
			return _index.GetEnumerator();
		}

		#endregion
	}
}